﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Build.Workflow;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Build.Workflow.Activities;

namespace TfsBuildLauncher
{
    public class Program
    {
        private static List<Type> allTypes;

        static int Main(string[] args)
        {
            Console.WriteLine("TfsBuildLauncher - a utility to start TFS builds with custom parameters");
            var arguments = new ArgumentsParser(args);

            Arg<Uri> tfsUriArg = arguments.Declare<Uri>("collection", IsRequired: true);
            Arg<string> teamProjectNameArg = arguments.Declare<string>("project", IsRequired: true);
            Arg<string> buildNameArg = arguments.Declare<string>("build", IsRequired: true);
            Arg<string> singleParametersKeyValuesArg = arguments.Declare<string>("p", IsRequired: false, IsList: true);
            // TODO : implement passing raw XML blocks on the command line rawParametersKeyValuesArg.Values
            // arg:  /raw:rawParameterXml
            // Arg<string> rawParametersKeyValuesArg = arguments.Declare<string>("raw", IsRequired: false, IsList: true);

            if (!arguments.IsValid)
            {
                Console.WriteLine(arguments.ErrorMessage);
                Console.WriteLine();
                // TODO: implement  /file:processParametersFile to pass an xml file with all the paremeters
                Console.WriteLine("Usage: TfsBuildLauncher.exe /collection:tfsUrl /project:teamProjectName /build:buildDefinition /p:paramName(paramType)=paramValue");
                Console.WriteLine("   Starts a TFS build with custom workflow parameters");
                Console.WriteLine();
                Console.WriteLine("   /p: may be used sereveral times to define multiple variables values");
                Console.WriteLine("       ex: /p:DisableTests(Boolean)=true /p:Verbosity(BuildVerbosity)=Detailed");
                Console.WriteLine();
                Console.WriteLine("Supported types: any non array type that can be natively converted from string (Boolean,Int32)");
                Console.WriteLine("and string arrays (System.String[]), please use full type name for this one.");
                Console.WriteLine("  - for StringList et System.String[], use comas as delimiters.");
                Console.WriteLine("     ex: /p:MyTags(StringList)=1st,2nd,OtherTag /p:MyList(System.String[])=Ls1,Ls2");
                Console.WriteLine();
                return 1;
            }

            try
            {
                IDictionary<string, object> parameters = ParseParameters(singleParametersKeyValuesArg.Values);
                QueueNewBuild(tfsUriArg.Value, teamProjectNameArg.Value, buildNameArg.Value, parameters);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
                Console.WriteLine("Exception: " + ex.Message);
                return 1;
            }
            return 0;
        }

        private static List<Type> AllTypes
        {
            get
            {
                if (allTypes == null)
                {
                    Assembly.Load("Microsoft.TeamFoundation.Build.Workflow");
                    Assembly.Load("Microsoft.TeamFoundation.Build.Client");
                    Assembly.Load("Microsoft.TeamFoundation.Client");
                    // very costly but we need it
                    allTypes = new List<Type>(10000);
                    foreach(Assembly a in AppDomain.CurrentDomain.GetAssemblies())
                    {
                        allTypes.AddRange(a.GetTypes());
                    }
                }
                return allTypes;
            }
        }

        public static Type FindType(string name)
        {
            Type type = Type.GetType(name);
            if (type == null)
            {
                type = AllTypes.Where(t => t.FullName == name).FirstOrDefault();
                if (type == null)
                {
                    type = AllTypes.Where(t => t.Name == name).FirstOrDefault();
                    if (type == null)
                    {
                        throw new ArgumentException("Type not found: " + name);
                    }
                }
            }
            return type;
        }

        public static IDictionary<string, object> ParseParameters(List<string> parametersKeyValues)
        {
            var parameters = new Dictionary<string, object>();
            Regex keyValueRegex = new Regex("^(?<key>[^=\\(]+)(\\((?<type>.+)\\))?=(?<value>.*)$");

            foreach (string keyValue in parametersKeyValues)
            {
                Match match = keyValueRegex.Match(keyValue);
                if (!match.Success)
                {
                    throw new ArgumentException("Invalid build parameter argument " + keyValue);
                }
                string name = match.Groups["key"].Value;
                string originalValue = match.Groups["value"].Value;
                string type = match.Groups["type"].Value;
                object value = originalValue;
                if (!String.IsNullOrEmpty(type))
                {
                    Type targetType = FindType(type);
                    if (targetType.IsEnum)
                    {
                        value = Enum.Parse(targetType, originalValue);
                    }
                    else if (targetType.Name == "StringList")
                    {
                        value = new StringList(originalValue.Split(','));
                    }
                    // does not support yet generic arrays, only string arays
                    else if (targetType.Name == "String[]")
                    {
                        value = originalValue.Split(',');
                    }
                    else
                    {
                        value = Convert.ChangeType(value, targetType);
                    }
                }
                else
                {
                    if (String.IsNullOrEmpty((string)value))
                    {
                        value = null;
                    }
                }
                parameters.Add(name, value);
            }

            return parameters;
        }

        private static void QueueNewBuild(Uri tfsUri, string teamProjectName, string buildName, IDictionary<string, object> parameters)
        {
            Console.WriteLine("Connecting to " + tfsUri);
            using (TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(tfsUri))
            {
                tpc.Authenticate();

                IBuildServer buildServer = tpc.GetService<IBuildServer>();
                
                IBuildDefinition buildDef = buildServer.GetBuildDefinition(teamProjectName, buildName);
                
                if (buildDef != null)
                {
                    Console.WriteLine("Build definition found.");

                    //Console.WriteLine(buildDef.ProcessParameters);
                    IBuildRequest req = buildDef.CreateBuildRequest();

                    if (parameters.Keys.Count > 0)
                    {
                        IDictionary<String, Object> paramValues = WorkflowHelpers.DeserializeProcessParameters(req.ProcessParameters);
                        assignParameters(paramValues, parameters);
                        req.ProcessParameters = WorkflowHelpers.SerializeProcessParameters(paramValues);
                    }

                    buildServer.QueueBuild(req);
                    Console.WriteLine("Build queued!");
                }
            }
        }

        private static void assignParameters(IDictionary<String, Object> paramValues, IDictionary<String, Object> argumentParameters)
        {
            foreach (string paramName in argumentParameters.Keys)
            {

                if (argumentParameters[paramName] == null)
                {
                    // if the parameter value is volontarily empty, we just remove the build process parameter
                    if (paramValues.ContainsKey(paramName))
                    {
                        paramValues.Remove(paramName);
                    }
                }
                else
                {
                    // otherwise simply overwrite the parameter's value
                    paramValues[paramName] = argumentParameters[paramName];
                }
            }
        }
    }
}
